/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.painting.resources;

import android.graphics.Canvas;
import android.graphics.Paint;

import com.hercules.android.receiver.LogUtil;
import com.hercules.android.receiver.model.objects.XREHorizontalAlignment;
import com.hercules.android.receiver.model.objects.XREResourceOptions;
import com.hercules.android.receiver.model.objects.XREText;
import com.hercules.android.receiver.model.objects.XRETextTruncStyle;
import com.hercules.android.receiver.model.objects.XREVerticalAlignment;
import com.hercules.android.receiver.model.objects.XREView;
import com.hercules.android.receiver.painting.PaintersUtil;
import com.hercules.android.receiver.painting.XREObjectPainterException;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * Painter for {@link XREText} resource. Blinking cursor feature is implemented via thread that with some period changes
 * text of this resource and redraws it. Also text wrapping and text truncation are supported and used based on
 * corresponding {@link XREView} {@link XREResourceOptions}.
 * <p>
 * AndroidAssembly1 this class was added in Android Assembly 1
 * <p>
 * <b>Thread safety:</b> Not thread safe, because base class is not thread safe
 *
 * @author 7realm
 * @version 1.0
 */
public class XRETextPainter extends BaseXREResourcePainter implements Runnable {
    /** Represents the ellipsis suffix. */
    private static final String ELLIPSIS_SUFFIX = "...";
    /** Represents the default blinking cursor interval in ms. */
    private static final int DEFAULT_BLINKING_CURSOR_INTERVAL = 1000;
    /**
     * Represents whether the cursor should be shown. If the text do not have blinkingCursor property, it is always
     * false, otherwise it is toggled by this thread every 1 second.
     */
    private boolean cursorOn;
    /**
     * Represents the blinking cursor interval in milliseconds.
     * <p>
     * Can only be positive, accessed by getter and setter, used in run() method.
     */
    private int blinkingCursorInterval = DEFAULT_BLINKING_CURSOR_INTERVAL;

    /**
     * Represents the measured width of the text.
     * <p>
     * Is initialized at drawing time.
     */
    private int measuredWidth;

    /**
     * Represents the measured height of the text.
     * <p>
     * Is initialized at drawing time.
     */
    private int measuredHeigth;

    /**
     * Constructor for class {@link XRETextPainter}.
     *
     * @param xreResource the {@link XREText}
     */
    public XRETextPainter(XREText xreResource) {
        super(xreResource);
    }

    /**
     * Performs initialize actions.
     *
     * @throws IllegalStateException if required fields are not set
     * @throws XREObjectPainterException if error occurs while init procedure
     */
    @SuppressWarnings("boxing")
    @Override
    public void init() throws XREObjectPainterException {
        String methodName = "init";
        LogUtil.enter(getLogger(), this, methodName);

        try {
            PaintersUtil.checkState(getReceiver(), "receiver");
            PaintersUtil.checkState(getXREView(), "xreView");
            PaintersUtil.checkState(getAndroidView(), "androidView");

            XREText text = getXREText();
            if (text.isBlinkingCursor()) {
                // create a thread
                Thread thread = new Thread(this);

                // add the thread to extra painter data
                PaintersUtil.getPainterDataMap(getReceiver(), "blinkingCursorThreads").put(text.getId(), thread);

                // start the thread
                thread.start();
            } else {
                this.cursorOn = false;
            }
        } catch (IllegalStateException e) {
            throw LogUtil.error(getLogger(), this, methodName, e);
        } catch (XREObjectPainterException e) {
            throw LogUtil.error(getLogger(), this, methodName, e);
        }

        LogUtil.exit(getLogger(), this, methodName);
    }

    /**
     * Performs clean up.
     *
     * @throws IllegalStateException if required fields are not set
     * @throws XREObjectPainterException if error occurs while clean up
     */
    @SuppressWarnings("boxing")
    @Override
    public void finish() throws XREObjectPainterException {
        String methodName = "finish";
        LogUtil.enter(getLogger(), this, methodName);

        try {
            PaintersUtil.checkState(getReceiver(), "receiver");

            XREText text = getXREText();
            if (text.isBlinkingCursor()) {
                // get the thread from extra painter data
                Thread thread =
                    PaintersUtil
                        .getPainterDataValue(getReceiver(), "blinkingCursorThreads", text.getId(), Thread.class);

                if (thread != null) {
                    // remove the thread from extra painter data
                    PaintersUtil.getPainterDataMap(getReceiver(), "blinkingCursorThreads").remove(text.getId());

                    // stop the thread
                    thread.stop();
                }
            }
        } catch (IllegalStateException e) {
            throw LogUtil.error(getLogger(), this, methodName, e);
        } catch (XREObjectPainterException e) {
            throw LogUtil.error(getLogger(), this, methodName, e);
        }

        LogUtil.exit(getLogger(), this, methodName);
    }

    /**
     * This method is called in a separate thread to make the cursor "blinks". The thread is started when a
     * {@link XREText} with blinking cursor is created.
     */
    public void run() {
        String methodName = "run";
        LogUtil.enter(getLogger(), this, methodName);

        while (true) {
            cursorOn = !cursorOn;

            // invalidate view
            getAndroidView().postInvalidate();

            try {
                Thread.sleep(blinkingCursorInterval);
            } catch (InterruptedException e) {
                // exit loop
                break;
            }
        }
    }

    /**
     * Draw resource on surface.
     *
     * @param canvas the canvas to be used
     */
    @Override
    public void onDraw(Canvas canvas) {
        XREText xreText = getXREText();
        String text = xreText.getText();

        // if cursorOn is true, add the text with a "_"
        if (cursorOn) {
            text += "_";
        }

        // create paint using XREText properties
        Paint paint = PaintersUtil.createPaint(xreText);
        float textSize = -paint.getFontMetrics().top + Math.max(xreText.getLeading(), paint.getFontMetrics().bottom);

        // get view dimensions
        int width = getXREView().getWidth();
        int height = getXREView().getHeight();
        canvas.clipRect(0, 0, width, height);

        // get resource options
        XREResourceOptions options = getXREView().getResourceOptions();

        // apply textStartPos and textStopPos
        if (options != null) {
            if (options.getTextStopChar() == -1) {
                text = text.substring(options.getTextStartChar());
            } else {
                text = text.substring(options.getTextStartChar(), options.getTextStopChar());
            }
        }

        // AndroidAssembly3 added support for text intents
        // first and last line's intent
        int startIntent = options == null ? 0 : (int) options.getTextStartPos(0);
        int endIntent = options == null ? 0 : (int) options.getTextStopPos(0);

        // split text to lines
        List<String> lines = new LinkedList<String>(Arrays.asList(text.split("\n")));

        // wrap lines if needed
        PaintersUtil.wrapLines(options, lines, paint, width, startIntent, endIntent);

        // truncate text
        if (options != null && options.getTextTruncStyle() != null
            && options.getTextTruncStyle() != XRETextTruncStyle.NONE) {
            // remove all lines that are bellow bottom
            while (height < lines.size() * textSize) {
                int lastIndex = lines.size() - 1;
                lines.set(lastIndex - 1, lines.get(lastIndex - 1) + lines.get(lastIndex));
                lines.remove(lastIndex);
            }

            int i = 0;
            while (i < lines.size()) {
                String line = lines.get(i);

                int truncateIndex = paint.breakText(line, true, width, null);

                // update line if it is required
                if (truncateIndex < line.length()) {
                    // get length of ellipsis suffix
                    float ellipsisLength = paint.measureText(ELLIPSIS_SUFFIX);

                    // break text to fit new width
                    int curLineWidth = PaintersUtil.getCurrentLineWidth(width, startIntent, endIntent, lines, i);
                    truncateIndex = paint.breakText(line, true, curLineWidth - ellipsisLength, null);

                    // skip all characters to last white space
                    if (options.getTextTruncStyle() == XRETextTruncStyle.ELLIPSIS_AT_WORD_BOUNDARY) {
                        truncateIndex = PaintersUtil.findLastSpace(line, truncateIndex);
                    }

                    line = line.substring(0, truncateIndex) + ELLIPSIS_SUFFIX;
                    lines.set(i, line.toString());
                }
                i++;
            }
        }

        // get text height
        float textHeight = lines.size() * textSize > height ? height : lines.size() * textSize;

        // align text vertically
        float top = 0;
        if (options != null) {
        	XREVerticalAlignment verticalAlignment = (options.getVerticalAlign() == null ? 
        			XREVerticalAlignment.CENTER : options.getVerticalAlign());
            switch (verticalAlignment) {
            case CENTER:
                top = (height - textHeight) / 2;
                break;
            case BOTTOM:
                top = height - textHeight;
            default:
                break;
            }
        }

        // add vertical intent of first line
        if (options != null) {
            top += options.getTextStartPos(1);
        }

        float maxTextWidth = 0;

        // draw all lines of text
        for (int i = 0; i < lines.size(); i++) {
            String line = lines.get(i);

            // align horizontally
            float textWidth = paint.measureText(line);
            textWidth = textWidth > width ? width : textWidth;
            float left = 0;
            if (options != null) {
            	XREHorizontalAlignment horizontalAlignment = (options.getHorizontalAlign() == null ? 
            			XREHorizontalAlignment.CENTER : options.getHorizontalAlign());
                switch (horizontalAlignment) {
                case CENTER:
                    left = (width - textWidth) / 2;
                    break;
                case RIGHT:
                    left = width - textWidth;
                default:
                    break;
                }
            }

            // add start intent to first line
            canvas.drawText(line, left + (i == 0 ? startIntent : 0), top
                + (-paint.getFontMetrics().top + textSize * i ), paint);

            // update maximum width
            maxTextWidth = maxTextWidth > textWidth ? maxTextWidth : textWidth;
        }

        // update measured values
        measuredHeigth = (int) textHeight;
        measuredWidth = (int) maxTextWidth;

        getXREView().updateMeasuredExtent(getAndroidView());
    }

    /**
     * Gets the blinking cursor interval.
     *
     * @return the blinking cursor interval
     */
    public int getBlinkingCursorInterval() {
        return blinkingCursorInterval;
    }

    /**
     * Sets the blinking cursor interval.
     *
     * @param blinkingCursorInterval the blinking cursor interval
     * @throws IllegalArgumentException if the blinking cursor interval is negative
     */
    public void setBlinkingCursorInterval(int blinkingCursorInterval) {
        if (blinkingCursorInterval <= 0) {
            throw new IllegalArgumentException("The blinking cursor interval should be positive.");
        }

        this.blinkingCursorInterval = blinkingCursorInterval;
    }

    /**
     * Get resource's measured width.
     *
     * @return measured width of resource
     */
    @Override
    public int getMeasuredWidth() {
        return measuredWidth;
    }

    /**
     * Get resource's measured height.
     *
     * @return measured height of resource
     */
    @Override
    public int getMeasuredHeight() {
        return measuredHeigth;
    }

    /**
     * Gets {@link XREText}.
     *
     * @return the {@link XREText} resource
     */
    private XREText getXREText() {
        return (XREText) getXREResource();
    }
}
